import logging
import json
import os
import yaml
from flask import make_response, request, url_for, send_from_directory
from funcy import project, partial

from flask_restful import abort
from redash import models
from redash import settings
from redash.handlers.base import (
    BaseResource,
    get_object_or_404,
    paginate,
    filter_by_tags,
    order_results as _order_results,
)
from redash.permissions import (
    has_access,
    not_view_only,
    can_modify,
    require_admin,
    require_admin_or_owner,
    require_object_modify_permission,
    require_permission,
)
from redash.security import csp_allows_embeding
from redash.serializers import (
    DashboardSerializer,
    public_dashboard,
)
from sqlalchemy.orm.exc import StaleDataError
from redash.utils import (
    JSONEncoder, 
    content_disposition_filenames,
    gen_query_hash,
)
from redash.utils import json_dumps, json_loads


# Ordering map for relationships
order_map = {
    "name": "lowercase_name",
    "-name": "-lowercase_name",
    "created_at": "created_at",
    "-created_at": "-created_at",
}

order_results = partial(
    _order_results, default_order="-created_at", allowed_orders=order_map
)


class DashboardListResource(BaseResource):
    @require_permission("list_dashboards")
    def get(self):
        """
        Lists all accessible dashboards.

        :qparam number page_size: Number of queries to return per page
        :qparam number page: Page number to retrieve
        :qparam number order: Name of column to order by
        :qparam number q: Full text search term

        Responds with an array of :ref:`dashboard <dashboard-response-label>`
        objects.
        """
        search_term = request.args.get("q")

        if search_term:
            results = models.Dashboard.search(
                self.current_org,
                self.current_user.group_ids,
                self.current_user.id,
                search_term,
            )
        else:
            results = models.Dashboard.all(
                self.current_org, self.current_user.group_ids, self.current_user.id
            )

        results = filter_by_tags(results, models.Dashboard.tags)

        # order results according to passed order parameter,
        # special-casing search queries where the database
        # provides an order by search rank
        ordered_results = order_results(results, fallback=not bool(search_term))

        page = request.args.get("page", 1, type=int)
        page_size = request.args.get("page_size", 25, type=int)

        response = paginate(
            ordered_results,
            page=page,
            page_size=page_size,
            serializer=DashboardSerializer,
        )

        if search_term:
            self.record_event(
                {"action": "search", "object_type": "dashboard", "term": search_term}
            )
        else:
            self.record_event({"action": "list", "object_type": "dashboard"})

        return response

    @require_permission("create_dashboard")
    def post(self):
        """
        Creates a new dashboard.

        :<json string name: Dashboard name

        Responds with a :ref:`dashboard <dashboard-response-label>`.
        """
        dashboard_properties = request.get_json(force=True)
        dashboard = models.Dashboard(
            name=dashboard_properties["name"],
            org=self.current_org,
            user=self.current_user,
            is_draft=True,
            layout= "[]" if dashboard_properties["allowOverlap"] == False else "{\"allowOverlap\": true}",
        )
        models.db.session.add(dashboard)
        models.db.session.commit()
        return DashboardSerializer(dashboard).serialize()


class DashboardCopyResource(BaseResource):
    @require_permission("create_dashboard")
    def post(self):
        dashboard_properties = request.get_json(force=True)
        old_d = models.Dashboard.get_by_id(dashboard_properties["id"])

        if old_d.template_type == 2:
            pathname = "./redash/excel/" + old_d.template_logo.replace(".png", ".xlsx")
            path = os.path.abspath(pathname)

            if settings.SQLALCHEMY_DATABASE_URI.find("mysql") == 0:
                jsonfile = path.replace(".xlsx", "/mysql.json")
            else:
                jsonfile = path.replace(".xlsx", "/postgres.json")

            if os.path.isfile(jsonfile) == False:
                abort(400, message="未找到报表模板文件，请联系管理员。")

            with open(jsonfile, 'r', encoding='utf8') as f:
                filedata = json.load(f)
                sql = "select " + ("" if settings.SQLALCHEMY_DATABASE_URI.find("mysql")==0 else "public.") + "dashboard_restore(:file_data,:user_id,:org_id,:flag,:dashboardname)"
                try:
                    rs = models.db.session.execute(sql, {
                        "file_data": filedata if settings.SQLALCHEMY_DATABASE_URI.find("mysql")==0 else json.dumps(filedata),
                        "user_id": self.current_user.id, 
                        "org_id": self.current_org.id, 
                        "flag": True,
                        "dashboardname": dashboard_properties["name"],
                        }).fetchall()
                except Exception as r: 
                    logging.exception(str(r))
                    abort(400, message="报表导入异常，请联系管理员。")

                if rs is None:
                    abort(400, message="报表导入异常，请联系管理员。")

                if settings.SQLALCHEMY_DATABASE_URI.find("mysql")==0:
                    dashboard_id =  json.loads(rs[0][0])[0]['dashboardid']
                else:
                    dashboard_id = rs[0][0][0]['dashboardid']
                
                dashboard = models.Dashboard.get_by_id(dashboard_id)

                #模板excel拷贝
                basepath = os.path.abspath(settings.DAZ_FILES) 
                if not os.path.exists(basepath):
                    try:
                        os.mkdir(basepath)
                    except Exception as e:
                        logging.exception(e)
                        return None                

                basepath = os.path.abspath(os.path.join(settings.DAZ_FILES, self.current_org.slug)) 
                if not os.path.exists(basepath):
                    try:
                        os.mkdir(basepath)
                    except Exception as e:
                        logging.exception(e)
                        return None      

                filename = str(self.current_user.id) + "_" + str(dashboard.id) + "_template.xls"
                fullname = os.path.join(basepath, filename)
                if os.path.isfile(fullname):
                    os.remove(fullname)

                with open(path, 'rb') as rstream:
                    container = rstream.read()
                    with open(fullname, 'wb') as wstream:
                        wstream.write(container)

                #改数据源文件
                for widget in dashboard.widgets:
                    widget = models.Widget.get_by_id(widget.id)
                    if widget.visualization is not None:
                        visualization = models.Visualization.get_by_id(widget.visualization.id)
                        query = models.Query.by_id(visualization.query_id)
                        new_query_text = query.query_text.replace(pathname, self.current_org.slug + "/" + filename)
                        query.query_text = new_query_text
                        query.query_hash = gen_query_hash(new_query_text), 
                        models.db.session.add(query)
                
                models.db.session.commit()

        else:
            dashboard = models.Dashboard(
                name=dashboard_properties["name"],
                org=self.current_org,
                user=self.current_user,
                is_draft=True,
                template_type=old_d.template_type,
                template_logo="",
                layout="[]",
            )
            models.db.session.add(dashboard)

            if old_d.org_id != self.current_org.id:
                for widget in old_d.widgets:
                    widget = models.Widget.get_by_id(widget.id)
                    if widget.visualization is not None:
                        visualization = models.Visualization.get_by_id(widget.visualization.id)
                        query = models.Query.by_id(visualization.query_id)
                        forked_query = query.fork(self.current_user, query.name + "：" + dashboard_properties["name"], query.data_source, False, visualization.id)

                    fork_widget = models.Widget(
                        dashboard = dashboard,
                        visualization = forked_query.visualizations[1] if widget.visualization is not None else None,
                        width = widget.width,
                        text = widget.text,
                        options = widget.options,
                        template_type = 0,
                        template_name = widget.template_name,
                        template_logo = widget.template_logo,
                    )
                    models.db.session.add(fork_widget)

            if old_d.org_id == self.current_org.id:    
                sql = "insert into widgets (updated_at,created_at,visualization_id,text,width,options,dashboard_id,template_type,template_name,template_logo) "\
                    "select now(),now(),visualization_id,text,width,options," +\
                    str(dashboard.id) + " as dashboard_id,0 as template_type,template_name,template_logo from widgets where dashboard_id=" + str(dashboard_properties["id"])
                models.db.session.execute(sql)

            models.db.session.commit()

        return DashboardSerializer(dashboard).serialize()


class DashboardExportResource(BaseResource):
    @staticmethod
    def add_cors_headers(headers):
        if "Origin" in request.headers:
            origin = request.headers["Origin"]

            if set(["*", origin]) & settings.ACCESS_CONTROL_ALLOW_ORIGIN:
                headers["Access-Control-Allow-Origin"] = origin
                headers["Access-Control-Allow-Credentials"] = str(
                    settings.ACCESS_CONTROL_ALLOW_CREDENTIALS
                ).lower()

    @require_permission("edit_dashboard")
    def get(self, dashboard_id=None):
        sql = "select " + ("" if settings.SQLALCHEMY_DATABASE_URI.find("mysql")==0 else "public.") + "dashboard_backup('" + str(dashboard_id) + "')"
        try:
            rs = models.db.session.execute(sql).fetchall()
        except Exception as r: 
            logging.exception(str(r))
            abort(400, message="报表导出异常，请联系管理员。")
        if rs is None:
            abort(400, message="报表导出异常，请联系管理员。")

        data = json.dumps(rs[0][0], cls=JSONEncoder) 
        headers = {"Content-Type": "application/json"}
        response = make_response(data, 200, headers)

        if len(settings.ACCESS_CONTROL_ALLOW_ORIGIN) > 0:
            self.add_cors_headers(response.headers)

        filename = "redash_dashboard_" + str(dashboard_id) + ".json"

        filenames = content_disposition_filenames(filename)
        response.headers.add("Content-Disposition", "attachment", **filenames)

        return response

    @require_permission("edit_dashboard")
    def post(self):
        req = request.get_json(force=True)

        sql = "select " + ("" if settings.SQLALCHEMY_DATABASE_URI.find("mysql")==0 else "public.") + "dashboard_backup('" + ','.join([str(r) for r in req]) + "')"
        try:
            rs = models.db.session.execute(sql).fetchall()
        except Exception as r: 
            logging.exception(str(r))
            abort(400, message="报表导出异常，请联系管理员。")
        if rs is None:
            abort(400, message="报表导出异常，请联系管理员。")

        return json.dumps(rs[0][0], cls=JSONEncoder) 


class DashboardImportResource(BaseResource):
    @require_permission("create_dashboard")
    def post(self):
        for file in request.files.getlist('file'):
            filedata = json.loads(file.read())
            sql = "select " + ("" if settings.SQLALCHEMY_DATABASE_URI.find("mysql")==0 else "public.") + "dashboard_restore(:file_data,:user_id,:org_id)"
            try:
                rs = models.db.session.execute(sql, {
                    "file_data": filedata if settings.SQLALCHEMY_DATABASE_URI.find("mysql")==0 else json.dumps(filedata),
                    "user_id": self.current_user.id, 
                    "org_id": self.current_org.id, 
                    }).fetchall()
                models.db.session.commit()
            except Exception as r: 
                logging.exception(str(r))
                abort(400, message="报表导入异常，请联系管理员。")

            if rs is None:
                abort(400, message="报表导入异常，请联系管理员。")

        if settings.SQLALCHEMY_DATABASE_URI.find("mysql")==0:
            data =  json.loads(rs[0][0])[0]
        else:
            data = rs[0][0][0]
        return data


class DashboardResource(BaseResource):
    def get(self, dashboard_id=None):
        """
        Retrieves a dashboard.

        :qparam number id: Id of dashboard to retrieve.

        .. _dashboard-response-label:

        :>json number id: Dashboard ID
        :>json string name:
        :>json string slug:
        :>json number user_id: ID of the dashboard creator
        :>json string created_at: ISO format timestamp for dashboard creation
        :>json string updated_at: ISO format timestamp for last dashboard modification
        :>json number version: Revision number of dashboard
        :>json boolean dashboard_filters_enabled: Whether filters are enabled or not
        :>json boolean is_archived: Whether this dashboard has been removed from the index or not
        :>json boolean is_draft: Whether this dashboard is a draft or not.
        :>json array layout: Array of arrays containing widget IDs, corresponding to the rows and columns the widgets are displayed in
        :>json array widgets: Array of arrays containing :ref:`widget <widget-response-label>` data

        .. _widget-response-label:

        Widget structure:

        :>json number widget.id: Widget ID
        :>json number widget.width: Widget size
        :>json object widget.options: Widget options
        :>json number widget.dashboard_id: ID of dashboard containing this widget
        :>json string widget.text: Widget contents, if this is a text-box widget
        :>json object widget.visualization: Widget contents, if this is a visualization widget
        :>json string widget.created_at: ISO format timestamp for widget creation
        :>json string widget.updated_at: ISO format timestamp for last widget modification
        """
        if request.args.get("legacy") is not None:
            fn = models.Dashboard.get_by_slug_and_org
        else:
            fn = models.Dashboard.get_by_id_and_org

        dashboard = get_object_or_404(fn, dashboard_id, self.current_org)
        response = DashboardSerializer(
            dashboard, with_widgets=True, user=self.current_user
        ).serialize()

        api_key = models.ApiKey.get_by_object(dashboard)
        if api_key:
            response["public_url"] = settings.HOST + url_for(
                "redash.public_dashboard",
                token=api_key.api_key,
                org_slug=self.current_org.slug,
                _external= True if settings.HOST == "" else False,
            )
            response["api_key"] = api_key.api_key

        response["can_edit"] = can_modify(dashboard, self.current_user)

        self.record_event(
            {"action": "view", "object_id": dashboard.id, "object_type": "dashboard"}
        )

        return response

    @require_permission("edit_dashboard")
    def post(self, dashboard_id):
        """
        Modifies a dashboard.

        :qparam number id: Id of dashboard to retrieve.

        Responds with the updated :ref:`dashboard <dashboard-response-label>`.

        :status 200: success
        :status 409: Version conflict -- dashboard modified since last read
        """
        dashboard_properties = request.get_json(force=True)
        # TODO: either convert all requests to use slugs or ids
        dashboard = models.Dashboard.get_by_id_and_org(dashboard_id, self.current_org)

        require_object_modify_permission(dashboard, self.current_user)

        updates = project(
            dashboard_properties,
            (
                "name",
                "layout",
                "version",
                "tags",
                "is_draft",
                "is_archived",
                "dashboard_filters_enabled",
            ),
        )

        # SQLAlchemy handles the case where a concurrent transaction beats us
        # to the update. But we still have to make sure that we're not starting
        # out behind.
        if "version" in updates and updates["version"] != dashboard.version:
            abort(409)

        updates["changed_by"] = self.current_user
        if ("layout" in dashboard_properties and dashboard_properties["layout"] != "[]"):
            updates["layout"] = json_dumps(dashboard_properties["layout"])

        self.update_model(dashboard, updates)
        models.db.session.add(dashboard)
        try:
            models.db.session.commit()
        except StaleDataError:
            abort(409)

        result = DashboardSerializer(
            dashboard, with_widgets=True, user=self.current_user
        ).serialize()

        self.record_event(
            {"action": "edit", "object_id": dashboard.id, "object_type": "dashboard"}
        )

        return result

    @require_permission("edit_dashboard")
    def delete(self, dashboard_id):
        """
        Archives a dashboard.

        :qparam number id: Id of dashboard to retrieve.

        Responds with the archived :ref:`dashboard <dashboard-response-label>`.
        """
        dashboard = models.Dashboard.get_by_id_and_org(dashboard_id, self.current_org)
        dashboard.is_archived = True
        dashboard.record_changes(changed_by=self.current_user)
        models.db.session.add(dashboard)
        d = DashboardSerializer(
            dashboard, with_widgets=True, user=self.current_user
        ).serialize()
        models.db.session.commit()

        self.record_event(
            {"action": "archive", "object_id": dashboard.id, "object_type": "dashboard"}
        )

        return d


class DashboardRemoveResource(BaseResource):
    @require_permission("edit_dashboard")
    def delete(self, dashboard_id):
        """
        Remove a dashboard.

        :qparam number id: Id of dashboard to retrieve.

        Responds with the removed :ref:`dashboard <dashboard-response-label>`.
        """
        dashboard = models.Dashboard.get_by_id_and_org(dashboard_id, self.current_org)
        sql = "delete from widgets where dashboard_id=" + str(dashboard_id)
        models.db.session.execute(sql)
        models.db.session.delete(dashboard)
        models.db.session.commit()

        self.record_event(
            {"action": "remove", "object_id": dashboard.id, "object_type": "dashboard"}
        )


class PublicDashboardResource(BaseResource):
    decorators = BaseResource.decorators + [csp_allows_embeding]

    def get(self, token):
        """updates
        Retrieve a public dashboard.

        :param token: An API key for a public dashboard.
        :>json array widgets: An array of arrays of :ref:`public widgets <public-widget-label>`, corresponding to the rows and columns the widgets are displayed in
        """
        if self.current_org.get_setting("disable_public_urls"):
            abort(400, message="分享链接已禁用.")

        if not isinstance(self.current_user, models.ApiUser):
            api_key = get_object_or_404(models.ApiKey.get_by_api_key, token)
            dashboard = api_key.object
        else:
            dashboard = self.current_user.object

        return public_dashboard(dashboard)


class DashboardShareResource(BaseResource):
    def post(self, dashboard_id):
        """
        Allow anonymous access to a dashboard.

        :param dashboard_id: The numeric ID of the dashboard to share.
        :>json string public_url: The URL for anonymous access to the dashboard.
        :>json api_key: The API key to use when accessing it.
        """
        dashboard = models.Dashboard.get_by_id_and_org(dashboard_id, self.current_org)
        require_admin_or_owner(dashboard.user_id)
        api_key = models.ApiKey.create_for_object(dashboard, self.current_user)
        
        share_state = request.get_json(force=True)
        if dashboard.layout == "[]":
            dashboard.layout = json_dumps(share_state)
        else:
            dashboard.layout =  json_dumps(dict(json_loads(dashboard.layout), **share_state))

        models.db.session.flush()
        models.db.session.commit()

        public_url = settings.HOST + url_for(
            "redash.public_dashboard",
            token=api_key.api_key,
            org_slug=self.current_org.slug,
            _external= True if settings.HOST == "" else False,
        )

        self.record_event(
            {
                "action": "activate_api_key",
                "object_id": dashboard.id,
                "object_type": "dashboard",
            }
        )

        return {"public_url": public_url, "api_key": api_key.api_key}

    def delete(self, dashboard_id):
        """
        Disable anonymous access to a dashboard.

        :param dashboard_id: The numeric ID of the dashboard to unshare.
        """
        dashboard = models.Dashboard.get_by_id_and_org(dashboard_id, self.current_org)
        require_admin_or_owner(dashboard.user_id)
        api_key = models.ApiKey.get_by_object(dashboard)

        if api_key:
            api_key.active = False
            models.db.session.add(api_key)
            models.db.session.commit()

        self.record_event(
            {
                "action": "deactivate_api_key",
                "object_id": dashboard.id,
                "object_type": "dashboard",
            }
        )


class DashboardTagsResource(BaseResource):
    @require_permission("list_dashboards")
    def get(self):
        """
        Lists all accessible dashboards.
        """
        if settings.SQLALCHEMY_DATABASE_URI.find("mysql") == 0:
            return {"tags": []}
        else:
            tags = models.Dashboard.all_tags(self.current_org, self.current_user)
            return {"tags": [{"name": name, "count": count} for name, count in tags]}


class DashboardFavoriteListResource(BaseResource):
    def get(self):
        search_term = request.args.get("q")

        if search_term:
            base_query = models.Dashboard.search(
                self.current_org,
                self.current_user.group_ids,
                self.current_user.id,
                search_term,
            )
            favorites = models.Dashboard.favorites(
                self.current_user, base_query=base_query
            )
        else:
            favorites = models.Dashboard.favorites(self.current_user)

        favorites = filter_by_tags(favorites, models.Dashboard.tags)

        # order results according to passed order parameter,
        # special-casing search queries where the database
        # provides an order by search rank
        favorites = order_results(favorites, fallback=not bool(search_term))

        page = request.args.get("page", 1, type=int)
        page_size = request.args.get("page_size", 25, type=int)
        # TODO: we don't need to check for favorite status here
        response = paginate(favorites, page, page_size, DashboardSerializer)

        self.record_event(
            {
                "action": "load_favorites",
                "object_type": "dashboard",
                "params": {
                    "q": search_term,
                    "tags": request.args.getlist("tags"),
                    "page": page,
                },
            }
        )

        return response


class DashboardTemplateListResource(BaseResource):
    def get(self):
        search_term = request.args.get("q")

        if search_term:
            templates = models.Dashboard.templates(search_term)
        else:
            templates = models.Dashboard.templates()

        templates = filter_by_tags(templates, models.Dashboard.tags)

        # order results according to passed order parameter,
        # special-casing search queries where the database
        # provides an order by search rank
        templates = order_results(templates, fallback=not bool(search_term))

        page = request.args.get("page", 1, type=int)
        page_size = request.args.get("page_size", 25, type=int)
        # TODO: we don't need to check for favorite status here
        response = paginate(templates, page, page_size, DashboardSerializer)

        self.record_event(
            {
                "action": "load_favorites",
                "object_type": "dashboard",
                "params": {
                    "q": search_term,
                    "tags": request.args.getlist("tags"),
                    "page": page,
                },
            }
        )

        return response


def error_response(message, http_status=400):
    return {"job": {"status": 4, "error": message}}, http_status


error_messages = {
    "no_permission": error_response(
        "没有权限运行当前查询。", 403
    ),
}


class DashboardTemplateResource(BaseResource):
    def get(self, dashboard_id):
        d = models.Dashboard.get_by_id(dashboard_id)
        q = models.Query.get_by_id(d.widgets[0].visualization.query_id)

        path = ""
        args = {}
        try:
            args = yaml.safe_load(q.query_text)
            path = args['url']
            args.pop('url', None)
            if "http://" not in path and "https://" not in path and "./redash/excel/" not in path:
                path = os.path.join(settings.DAZ_FILES, path)
        except:
            pass

        if path == "":
            path = os.path.join(settings.DAZ_FILES, q.query_text)
        
        dirname,filename = os.path.split(os.path.abspath(path))
        return send_from_directory(dirname, filename=filename, as_attachment=True)

    def post(self, dashboard_id):
        d = models.Dashboard.get_by_id(dashboard_id)
        q = models.Query.get_by_id(d.widgets[0].visualization.query_id)

        if not has_access(d, self.current_user, not_view_only):
            return error_messages["no_permission"]

        file = request.files['file']

        basepath = os.path.abspath(settings.DAZ_FILES) 
        basepath = os.path.abspath(os.path.join(settings.DAZ_FILES, self.current_org.slug)) 

        filename = str(self.current_user.id) + "_" + str(d.id) + "_template.xls"
        fullname = os.path.join(basepath, filename)
        if os.path.isfile(fullname):
            os.remove(fullname)

        file.save(fullname)
        return None


class DashboardPublishResource(BaseResource):
    def get(self):
        dashboards = models.Dashboard.get_publish(self.current_org, self.current_user)

        response = {}
        for ds in dashboards:
            if ds.id in response:
                continue

            try:
                d = ds.to_dict()
                response[ds.id] = d
            except AttributeError:
                abort(409)

        return sorted(list(response.values()), key=lambda d: d["name"].lower())


def includedashboard(dashboards, dashboard_id):
    for ds in dashboards:
        if ds.id == dashboard_id:
            return True

    return False


def hasPermission(menus, dashboards):
    for ms in menus:
        if ms["dashboard"] != "" and ms["dashboard"] != "POP" and ms["dashboard"] != "URL" and not includedashboard(dashboards, int(ms["dashboard"])):
            ms["dashboard"] = ""

        try:
            ms["children"] = hasPermission(ms["children"], dashboards)
        except:
            pass

    return menus


def doMore(dashboards):
    rs = []
    more = {'id':0, 'name': '更多…', 'icon': 'FolderOpenOutlined', 'children': []}
    i = 0
    m = 0
    for d in dashboards:
        if i % 10 == 0:
            if m>1: 
                rs.append(more) 
                more = {'id':0, 'name': '更多…', 'icon': 'FolderOpenOutlined', 'children': []}
            m = m + 1
        if i >= 10:
            more['children'].append(d.to_dict())
        else:
            rs.append(d.to_dict())
        i = i + 1
    
    if i>=10: rs.append(more)

    return rs


class DashboardPortalResource(BaseResource):
    def get(self):

        dashboards = models.Dashboard.get_publish_by_user(
            self.current_org,
            self.current_user.group_ids,
            self.current_user.id,
        )

        return doMore(dashboards)

    def post(self, code):
        dashboards = models.Dashboard.get_publish_by_user(
            self.current_org,
            self.current_user.group_ids,
            self.current_user.id,
        )

        portal = models.Portal.get_by_code_and_org(code, self.current_org)
        if portal is None:
            return None

        menus = portal.items                
        return hasPermission(menus, dashboards)